import { EventEmitter } from "./eventEmitter";

type ReadableStreamParserOption = {
  stream: ReadableStream<Uint8Array>;
};

export class ReadableStreamParser extends EventEmitter {
  plugins: Plugin[] = [];
  inProgress = false;

  use(plugin: Plugin) {
    if (this.inProgress) {
      throw new Error("Can not use plugin while the parser is in progress.");
    }
    plugin.bind(this);
    this.plugins.push(plugin);
  }

  async parse(stream: ReadableStream<Uint8Array>) {
    if (this.inProgress) {
      throw new Error("The parser is still in progress");
    }
    this.inProgress = true;
    try {
      const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
      while (true) {
        const chunk = await reader.read();
        if (chunk.done) {
          break;
        }
        this.emit("data", chunk.value);
      }
      this.emit("end");
    } catch (error) {
      this.emit("error", error);
    } finally {
      this.inProgress = false;
    }
  }
}

// plugins
interface Plugin {
  bind(parser: ReadableStreamParser): void;
}

type JsonParserOption = {
  beforeParse?: (part: string) => string;
  onParsed?: (part: any) => void;
};

export class JsonParser implements Plugin {
  private p = 0;
  private current = "";
  private escape = false;
  private inString = false;
  private option: Required<JsonParserOption>;
  result: any[] = [];
  constructor(option: JsonParserOption = {}) {
    const { beforeParse = (s) => s, onParsed = () => void 0 } = option;
    this.option = { beforeParse, onParsed };
  }

  bind(parser: ReadableStreamParser): void {
    parser.on("data", (data: string) => {
      this.onData(data);
    });
  }

  onData(data: string) {
    for (let i = 0, len = data.length; i < len; i++) {
      const c = data[i];
      if (c === '"' && !this.escape) {
        this.inString = !this.inString;
      }
      if (c === "{" && !this.escape && !this.inString) {
        this.p += 1;
      }
      if (this.p !== 0) {
        this.current += c;
      }
      if (this.p !== 0 && c === "}" && !this.escape && !this.inString) {
        this.p -= 1;
      }
      if (c === "\\" && !this.escape) {
        this.escape = true;
      } else {
        this.escape = false;
      }
      if (this.p === 0 && this.current !== "") {
        try {
          const str = this.option.beforeParse(this.current);
          const parsed = JSON.parse(str);
          this.option.onParsed(parsed);
          this.result.push(parsed);
        } catch (err) {
          console.log("try parsed:", this.current);
          console.error(err);
        }
        this.current = "";
      }
    }
  }
}
